1
2
3
4
5
6
7
8 import urllib
9 import time
10 import random
11 import urlparse
12 import hmac
13 import binascii
14 from types import ListType
15
16 try:
17 from urlparse import parse_qs
18 except ImportError:
19 from cgi import parse_qs
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 occured.'):
32
33 @property
35 """A hack to get around the deprecation errors in 2.6."""
36 return self._message
37
40
43
45 """Optional WWW-Authenticate header (401 error)"""
46 return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
47
50 """Escape a URL including any /."""
51 return urllib.quote(s, safe='~')
52
55 """Get seconds since epoch (UTC)."""
56 return int(time.time())
57
60 """Generate pseudorandom number."""
61 return ''.join([str(random.randint(0, 9)) for i in range(length)])
62
65 """Generate pseudorandom number."""
66 return ''.join([str(random.randint(0, 9)) for i in range(length)])
67
70 """A consumer of OAuth-protected services.
71
72 The OAuth consumer is a "third-party" service that wants to access
73 protected resources from an OAuth service provider on behalf of an end
74 user. It's kind of the OAuth client.
75
76 Usually a consumer must be registered with the service provider by the
77 developer of the consumer software. As part of that process, the service
78 provider gives the consumer a *key* and a *secret* with which the consumer
79 software can identify itself to the service. The consumer will include its
80 key in each request to identify itself, but will use its secret only when
81 signing requests, to prove that the request is from that particular
82 registered consumer.
83
84 Once registered, the consumer can then use its consumer credentials to ask
85 the service provider for a request token, kicking off the OAuth
86 authorization process.
87 """
88
89 key = None
90 secret = None
91
93 self.key = key
94 self.secret = secret
95
96 if self.key is None or self.secret is None:
97 raise ValueError("Key and secret must be set.")
98
100 data = {
101 'oauth_consumer_key': self.key,
102 'oauth_consumer_secret': self.secret
103 }
104
105 return urllib.urlencode(data)
106
109 """An OAuth credential used to request authorization or a protected
110 resource.
111
112 Tokens in OAuth comprise a *key* and a *secret*. The key is included in
113 requests to identify the token being used, but the secret is used only in
114 the signature, to prove that the requester is who the server gave the
115 token to.
116
117 When first negotiating the authorization, the consumer asks for a *request
118 token* that the live user authorizes with the service provider. The
119 consumer then exchanges the request token for an *access token* that can
120 be used to access protected resources.
121 """
122
123 key = None
124 secret = None
125 callback = None
126 callback_confirmed = None
127 verifier = None
128
130 self.key = key
131 self.secret = secret
132
133 if self.key is None or self.secret is None:
134 raise ValueError("Key and secret must be set.")
135
139
145
147 if self.callback and self.verifier:
148
149 parts = urlparse.urlparse(self.callback)
150 scheme, netloc, path, params, query, fragment = parts[:6]
151 if query:
152 query = '%s&oauth_verifier=%s' % (query, self.verifier)
153 else:
154 query = 'oauth_verifier=%s' % self.verifier
155 return urlparse.urlunparse((scheme, netloc, path, params,
156 query, fragment))
157 return self.callback
158
160 """Returns this token as a plain string, suitable for storage.
161
162 The resulting string includes the token's secret, so you should never
163 send or store this string where a third party can read it.
164 """
165
166 data = {
167 'oauth_token': self.key,
168 'oauth_token_secret': self.secret,
169 }
170
171 if self.callback_confirmed is not None:
172 data['oauth_callback_confirmed'] = self.callback_confirmed
173 return urllib.urlencode(data)
174
175 @staticmethod
177 """Deserializes a token from a string like one returned by
178 `to_string()`."""
179
180 if not len(s):
181 raise ValueError("Invalid parameter string.")
182
183 params = parse_qs(s, keep_blank_values=False)
184 if not len(params):
185 raise ValueError("Invalid parameter string.")
186
187 try:
188 key = params['oauth_token'][0]
189 except Exception:
190 raise ValueError("'oauth_token' not found in OAuth request.")
191
192 try:
193 secret = params['oauth_token_secret'][0]
194 except Exception:
195 raise ValueError("'oauth_token_secret' not found in "
196 "OAuth request.")
197
198 token = Token(key, secret)
199 try:
200 token.callback_confirmed = params['oauth_callback_confirmed'][0]
201 except KeyError:
202 pass
203 return token
204
207
210 name = attr.__name__
211
212 def getter(self):
213 try:
214 return self.__dict__[name]
215 except KeyError:
216 raise AttributeError(name)
217
218 def deleter(self):
219 del self.__dict__[name]
220
221 return property(getter, attr, deleter)
222
225
226 """The parameters and information for an HTTP request, suitable for
227 authorizing with OAuth credentials.
228
229 When a consumer wants to access a service's protected resources, it does
230 so using a signed HTTP request identifying itself (the consumer) with its
231 key, and providing an access token authorized by the end user to access
232 those resources.
233
234 """
235
236 http_method = HTTP_METHOD
237 http_url = None
238 version = VERSION
239
241 if method is not None:
242 self.method = method
243
244 if url is not None:
245 self.url = url
246
247 if parameters is not None:
248 self.update(parameters)
249
250 @setter
251 - def url(self, value):
252 parts = urlparse.urlparse(value)
253 scheme, netloc, path = parts[:3]
254
255
256 if scheme == 'http' and netloc[-3:] == ':80':
257 netloc = netloc[:-3]
258 elif scheme == 'https' and netloc[-4:] == ':443':
259 netloc = netloc[:-4]
260
261 if scheme != 'http' and scheme != 'https':
262 raise ValueError("Unsupported URL %s (%s)." % (value, scheme))
263
264 value = '%s://%s%s' % (scheme, netloc, path)
265 self.__dict__['url'] = value
266
267 @setter
269 self.__dict__['method'] = value.upper()
270
272 return self['oauth_timestamp'], self['oauth_nonce']
273
275 """Get any non-OAuth parameters."""
276 return dict([(k, v) for k, v in self.iteritems()
277 if not k.startswith('oauth_')])
278
280 """Serialize as a header for an HTTPAuth request."""
281 oauth_params = ((k, v) for k, v in self.items()
282 if k.startswith('oauth_'))
283 stringy_params = ((k, escape(str(v))) for k, v in oauth_params)
284 header_params = ('%s="%s"' % (k, v) for k, v in stringy_params)
285 params_header = ', '.join(header_params)
286
287 auth_header = 'OAuth realm="%s"' % realm
288 if params_header:
289 auth_header = "%s, %s" % (auth_header, params_header)
290
291 return {'Authorization': auth_header}
292
293 - def to_postdata(self):
294 """Serialize as post data for a POST request."""
295
296
297
298 return urllib.urlencode(self, True)
299
301 """Serialize as a URL for a GET request."""
302 return '%s?%s' % (self.url, self.to_postdata())
303
305 ret = self.get(parameter)
306 if ret is None:
307 raise Error('Parameter not found: %s' % parameter)
308
309 return ret
310
312 """Return a string that contains the parameters that must be signed."""
313
314 items = [(k, v if type(v) != ListType else sorted(v)) for \
315 k,v in sorted(self.items()) if k != 'oauth_signature']
316 encoded_str = urllib.urlencode(items, True)
317
318
319
320
321 return encoded_str.replace('+', '%20')
322
324 """Set the signature parameter to the result of sign."""
325
326 if 'oauth_consumer_key' not in self:
327 self['oauth_consumer_key'] = consumer.key
328
329 if token and 'oauth_token' not in self:
330 self['oauth_token'] = token.key
331
332 self['oauth_signature_method'] = signature_method.name
333 self['oauth_signature'] = signature_method.sign(self, consumer, token)
334
335 @classmethod
337 """Get seconds since epoch (UTC)."""
338 return str(int(time.time()))
339
340 @classmethod
342 """Generate pseudorandom number."""
343 return str(random.randint(0, 100000000))
344
345 @classmethod
346 - def from_request(cls, http_method, http_url, headers=None, parameters=None,
347 query_string=None):
348 """Combines multiple parameter sources."""
349 if parameters is None:
350 parameters = {}
351
352
353 if headers and 'Authorization' in headers:
354 auth_header = headers['Authorization']
355
356 if auth_header[:6] == 'OAuth ':
357 auth_header = auth_header[6:]
358 try:
359
360 header_params = cls._split_header(auth_header)
361 parameters.update(header_params)
362 except:
363 raise Error('Unable to parse OAuth parameters from '
364 'Authorization header.')
365
366
367 if query_string:
368 query_params = cls._split_url_string(query_string)
369 parameters.update(query_params)
370
371
372 param_str = urlparse.urlparse(http_url)[4]
373 url_params = cls._split_url_string(param_str)
374 parameters.update(url_params)
375
376 if parameters:
377 return cls(http_method, http_url, parameters)
378
379 return None
380
381 @classmethod
384 if not parameters:
385 parameters = {}
386
387 defaults = {
388 'oauth_consumer_key': consumer.key,
389 'oauth_timestamp': cls.make_timestamp(),
390 'oauth_nonce': cls.make_nonce(),
391 'oauth_version': cls.version,
392 }
393
394 defaults.update(parameters)
395 parameters = defaults
396
397 if token:
398 parameters['oauth_token'] = token.key
399
400 return Request(http_method, http_url, parameters)
401
402 @classmethod
405
406 if not parameters:
407 parameters = {}
408
409 parameters['oauth_token'] = token.key
410
411 if callback:
412 parameters['oauth_callback'] = callback
413
414 return cls(http_method, http_url, parameters)
415
416 @staticmethod
418 """Turn Authorization: header into parameters."""
419 params = {}
420 parts = header.split(',')
421 for param in parts:
422
423 if param.find('realm') > -1:
424 continue
425
426 param = param.strip()
427
428 param_parts = param.split('=', 1)
429
430 params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
431 return params
432
433 @staticmethod
435 """Turn URL string into parameters."""
436 parameters = parse_qs(param_str, keep_blank_values=False)
437 for k, v in parameters.iteritems():
438 parameters[k] = urllib.unquote(v[0])
439 return parameters
440
443 """A skeletal implementation of a service provider, providing protected
444 resources to requests from authorized consumers.
445
446 This class implements the logic to check requests for authorization. You
447 can use it with your web server or web framework to protect certain
448 resources with OAuth.
449 """
450
451 timestamp_threshold = 300
452 version = VERSION
453 signature_methods = None
454
455 - def __init__(self, signature_methods=None):
457
461
469
471 """Optional support for the authenticate header."""
472 return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
473
485
487 """Figure out the signature with some defaults."""
488 try:
489 signature_method = request.get_parameter('oauth_signature_method')
490 except:
491 signature_method = SIGNATURE_METHOD
492
493 try:
494
495 signature_method = self.signature_methods[signature_method]
496 except:
497 signature_method_names = ', '.join(self.signature_methods.keys())
498 raise Error(
499 'Signature method %s not supported try one of the following: %s' % (
500 signature_method, signature_method_names))
501
502 return signature_method
503
506
527
529 """Verify that timestamp is recentish."""
530 timestamp = int(timestamp)
531 now = int(time.time())
532 lapsed = now - timestamp
533 if lapsed > self.timestamp_threshold:
534 raise Error('Expired timestamp: given %d and now %s has a '
535 'greater difference than threshold %d' % (timestamp, now,
536 self.timestamp_threshold))
537
540 """A way of signing requests.
541
542 The OAuth protocol lets consumers and service providers pick a way to sign
543 requests. This interface shows the methods expected by the other `oauth`
544 modules for signing requests. Subclass it and implement its methods to
545 provide a new way to sign requests.
546 """
547
549 """Calculates the string that needs to be signed.
550
551 This method returns a 2-tuple containing the starting key for the
552 signing and the message to be signed. The latter may be used in error
553 messages to help clients debug their software.
554
555 """
556 raise NotImplementedError
557
558 - def sign(self, request, consumer, token):
559 """Returns the signature for the given request, based on the consumer
560 and token also provided.
561
562 You should use your implementation of `signing_base()` to build the
563 message to sign. Otherwise it may be less useful for debugging.
564
565 """
566 raise NotImplementedError
567
568 - def check(self, request, consumer, token, signature):
569 """Returns whether the given signature is the correct signature for
570 the given consumer and token signing the given request."""
571 built = self.sign(request, consumer, token)
572 return built == signature
573
576 name = 'HMAC-SHA1'
577
590
591 - def sign(self, request, consumer, token):
592 """Builds the base signature string."""
593 key, raw = self.signing_base(request, consumer, token)
594
595
596 try:
597 import hashlib
598 hashed = hmac.new(key, raw, hashlib.sha1)
599 except ImportError:
600 import sha
601 hashed = hmac.new(key, raw, sha)
602
603
604 return binascii.b2a_base64(hashed.digest())[:-1]
605
606 -class SignatureMethod_PLAINTEXT(SignatureMethod):
607
608 name = 'PLAINTEXT'
609
610 - def signing_base(self, request, consumer, token):
611 """Concatenates the consumer key and secret with the token's
612 secret."""
613 sig = '%s&' % escape(consumer.secret)
614 if token:
615 sig = sig + escape(token.secret)
616 return sig, sig
617
618 - def sign(self, request, consumer, token):
619 key, raw = self.signing_base(request, consumer, token)
620 return raw
621