Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(160)

Side by Side Diff: chrome/common/extensions/docs/examples/apps/hello-python/oauth2/__init__.py

Issue 3161030: Adding a sample CWS license server client written for Python App Engine. (Closed) Base URL: http://src.chromium.org/git/chromium.git
Patch Set: More sensible fallback for dev environment. Created 10 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 """
2 The MIT License
3
4 Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy
7 of this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights
9 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 copies of the Software, and to permit persons to whom the Software is
11 furnished to do so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in
14 all copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 THE SOFTWARE.
23 """
24
25 import urllib
26 import time
27 import random
28 import urlparse
29 import hmac
30 import binascii
31 import httplib2
32
33 try:
34 from urlparse import parse_qs, parse_qsl
35 except ImportError:
36 from cgi import parse_qs, parse_qsl
37
38
39 VERSION = '1.0' # Hi Blaine!
40 HTTP_METHOD = 'GET'
41 SIGNATURE_METHOD = 'PLAINTEXT'
42
43
44 class Error(RuntimeError):
45 """Generic exception class."""
46
47 def __init__(self, message='OAuth error occurred.'):
48 self._message = message
49
50 @property
51 def message(self):
52 """A hack to get around the deprecation errors in 2.6."""
53 return self._message
54
55 def __str__(self):
56 return self._message
57
58
59 class MissingSignature(Error):
60 pass
61
62
63 def build_authenticate_header(realm=''):
64 """Optional WWW-Authenticate header (401 error)"""
65 return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
66
67
68 def build_xoauth_string(url, consumer, token=None):
69 """Build an XOAUTH string for use in SMTP/IMPA authentication."""
70 request = Request.from_consumer_and_token(consumer, token,
71 "GET", url)
72
73 signing_method = SignatureMethod_HMAC_SHA1()
74 request.sign_request(signing_method, consumer, token)
75
76 params = []
77 for k, v in sorted(request.iteritems()):
78 if v is not None:
79 params.append('%s="%s"' % (k, escape(v)))
80
81 return "%s %s %s" % ("GET", url, ','.join(params))
82
83
84 def escape(s):
85 """Escape a URL including any /."""
86 return urllib.quote(s, safe='~')
87
88
89 def generate_timestamp():
90 """Get seconds since epoch (UTC)."""
91 return int(time.time())
92
93
94 def generate_nonce(length=8):
95 """Generate pseudorandom number."""
96 return ''.join([str(random.randint(0, 9)) for i in range(length)])
97
98
99 def generate_verifier(length=8):
100 """Generate pseudorandom number."""
101 return ''.join([str(random.randint(0, 9)) for i in range(length)])
102
103
104 class Consumer(object):
105 """A consumer of OAuth-protected services.
106
107 The OAuth consumer is a "third-party" service that wants to access
108 protected resources from an OAuth service provider on behalf of an end
109 user. It's kind of the OAuth client.
110
111 Usually a consumer must be registered with the service provider by the
112 developer of the consumer software. As part of that process, the service
113 provider gives the consumer a *key* and a *secret* with which the consumer
114 software can identify itself to the service. The consumer will include its
115 key in each request to identify itself, but will use its secret only when
116 signing requests, to prove that the request is from that particular
117 registered consumer.
118
119 Once registered, the consumer can then use its consumer credentials to ask
120 the service provider for a request token, kicking off the OAuth
121 authorization process.
122 """
123
124 key = None
125 secret = None
126
127 def __init__(self, key, secret):
128 self.key = key
129 self.secret = secret
130
131 if self.key is None or self.secret is None:
132 raise ValueError("Key and secret must be set.")
133
134 def __str__(self):
135 data = {'oauth_consumer_key': self.key,
136 'oauth_consumer_secret': self.secret}
137
138 return urllib.urlencode(data)
139
140
141 class Token(object):
142 """An OAuth credential used to request authorization or a protected
143 resource.
144
145 Tokens in OAuth comprise a *key* and a *secret*. The key is included in
146 requests to identify the token being used, but the secret is used only in
147 the signature, to prove that the requester is who the server gave the
148 token to.
149
150 When first negotiating the authorization, the consumer asks for a *request
151 token* that the live user authorizes with the service provider. The
152 consumer then exchanges the request token for an *access token* that can
153 be used to access protected resources.
154 """
155
156 key = None
157 secret = None
158 callback = None
159 callback_confirmed = None
160 verifier = None
161
162 def __init__(self, key, secret):
163 self.key = key
164 self.secret = secret
165
166 if self.key is None or self.secret is None:
167 raise ValueError("Key and secret must be set.")
168
169 def set_callback(self, callback):
170 self.callback = callback
171 self.callback_confirmed = 'true'
172
173 def set_verifier(self, verifier=None):
174 if verifier is not None:
175 self.verifier = verifier
176 else:
177 self.verifier = generate_verifier()
178
179 def get_callback_url(self):
180 if self.callback and self.verifier:
181 # Append the oauth_verifier.
182 parts = urlparse.urlparse(self.callback)
183 scheme, netloc, path, params, query, fragment = parts[:6]
184 if query:
185 query = '%s&oauth_verifier=%s' % (query, self.verifier)
186 else:
187 query = 'oauth_verifier=%s' % self.verifier
188 return urlparse.urlunparse((scheme, netloc, path, params,
189 query, fragment))
190 return self.callback
191
192 def to_string(self):
193 """Returns this token as a plain string, suitable for storage.
194
195 The resulting string includes the token's secret, so you should never
196 send or store this string where a third party can read it.
197 """
198
199 data = {
200 'oauth_token': self.key,
201 'oauth_token_secret': self.secret,
202 }
203
204 if self.callback_confirmed is not None:
205 data['oauth_callback_confirmed'] = self.callback_confirmed
206 return urllib.urlencode(data)
207
208 @staticmethod
209 def from_string(s):
210 """Deserializes a token from a string like one returned by
211 `to_string()`."""
212
213 if not len(s):
214 raise ValueError("Invalid parameter string.")
215
216 params = parse_qs(s, keep_blank_values=False)
217 if not len(params):
218 raise ValueError("Invalid parameter string.")
219
220 try:
221 key = params['oauth_token'][0]
222 except Exception:
223 raise ValueError("'oauth_token' not found in OAuth request.")
224
225 try:
226 secret = params['oauth_token_secret'][0]
227 except Exception:
228 raise ValueError("'oauth_token_secret' not found in "
229 "OAuth request.")
230
231 token = Token(key, secret)
232 try:
233 token.callback_confirmed = params['oauth_callback_confirmed'][0]
234 except KeyError:
235 pass # 1.0, no callback confirmed.
236 return token
237
238 def __str__(self):
239 return self.to_string()
240
241
242 def setter(attr):
243 name = attr.__name__
244
245 def getter(self):
246 try:
247 return self.__dict__[name]
248 except KeyError:
249 raise AttributeError(name)
250
251 def deleter(self):
252 del self.__dict__[name]
253
254 return property(getter, attr, deleter)
255
256
257 class Request(dict):
258
259 """The parameters and information for an HTTP request, suitable for
260 authorizing with OAuth credentials.
261
262 When a consumer wants to access a service's protected resources, it does
263 so using a signed HTTP request identifying itself (the consumer) with its
264 key, and providing an access token authorized by the end user to access
265 those resources.
266
267 """
268
269 version = VERSION
270
271 def __init__(self, method=HTTP_METHOD, url=None, parameters=None):
272 self.method = method
273 self.url = url
274 if parameters is not None:
275 self.update(parameters)
276
277 @setter
278 def url(self, value):
279 self.__dict__['url'] = value
280 if value is not None:
281 scheme, netloc, path, params, query, fragment = urlparse.urlparse(va lue)
282
283 # Exclude default port numbers.
284 if scheme == 'http' and netloc[-3:] == ':80':
285 netloc = netloc[:-3]
286 elif scheme == 'https' and netloc[-4:] == ':443':
287 netloc = netloc[:-4]
288 if scheme not in ('http', 'https'):
289 raise ValueError("Unsupported URL %s (%s)." % (value, scheme))
290
291 # Normalized URL excludes params, query, and fragment.
292 self.normalized_url = urlparse.urlunparse((scheme, netloc, path, Non e, None, None))
293 else:
294 self.normalized_url = None
295 self.__dict__['url'] = None
296
297 @setter
298 def method(self, value):
299 self.__dict__['method'] = value.upper()
300
301 def _get_timestamp_nonce(self):
302 return self['oauth_timestamp'], self['oauth_nonce']
303
304 def get_nonoauth_parameters(self):
305 """Get any non-OAuth parameters."""
306 return dict([(k, v) for k, v in self.iteritems()
307 if not k.startswith('oauth_')])
308
309 def to_header(self, realm=''):
310 """Serialize as a header for an HTTPAuth request."""
311 oauth_params = ((k, v) for k, v in self.items()
312 if k.startswith('oauth_'))
313 stringy_params = ((k, escape(str(v))) for k, v in oauth_params)
314 header_params = ('%s="%s"' % (k, v) for k, v in stringy_params)
315 params_header = ', '.join(header_params)
316
317 auth_header = 'OAuth realm="%s"' % realm
318 if params_header:
319 auth_header = "%s, %s" % (auth_header, params_header)
320
321 return {'Authorization': auth_header}
322
323 def to_postdata(self):
324 """Serialize as post data for a POST request."""
325 # tell urlencode to deal with sequence values and map them correctly
326 # to resulting querystring. for example self["k"] = ["v1", "v2"] will
327 # result in 'k=v1&k=v2' and not k=%5B%27v1%27%2C+%27v2%27%5D
328 return urllib.urlencode(self, True).replace('+', '%20')
329
330 def to_url(self):
331 """Serialize as a URL for a GET request."""
332 base_url = urlparse.urlparse(self.url)
333 try:
334 query = base_url.query
335 except AttributeError:
336 # must be python <2.5
337 query = base_url[4]
338 query = parse_qs(query)
339 for k, v in self.items():
340 query.setdefault(k, []).append(v)
341
342 try:
343 scheme = base_url.scheme
344 netloc = base_url.netloc
345 path = base_url.path
346 params = base_url.params
347 fragment = base_url.fragment
348 except AttributeError:
349 # must be python <2.5
350 scheme = base_url[0]
351 netloc = base_url[1]
352 path = base_url[2]
353 params = base_url[3]
354 fragment = base_url[5]
355
356 url = (scheme, netloc, path, params,
357 urllib.urlencode(query, True), fragment)
358 return urlparse.urlunparse(url)
359
360 def get_parameter(self, parameter):
361 ret = self.get(parameter)
362 if ret is None:
363 raise Error('Parameter not found: %s' % parameter)
364
365 return ret
366
367 def get_normalized_parameters(self):
368 """Return a string that contains the parameters that must be signed."""
369 items = []
370 for key, value in self.iteritems():
371 if key == 'oauth_signature':
372 continue
373 # 1.0a/9.1.1 states that kvp must be sorted by key, then by value,
374 # so we unpack sequence values into multiple items for sorting.
375 if hasattr(value, '__iter__'):
376 items.extend((key, item) for item in value)
377 else:
378 items.append((key, value))
379
380 # Include any query string parameters from the provided URL
381 query = urlparse.urlparse(self.url)[4]
382
383 url_items = self._split_url_string(query).items()
384 non_oauth_url_items = list([(k, v) for k, v in url_items if not k.start swith('oauth_')])
385 items.extend(non_oauth_url_items)
386
387 encoded_str = urllib.urlencode(sorted(items))
388 # Encode signature parameters per Oauth Core 1.0 protocol
389 # spec draft 7, section 3.6
390 # (http://tools.ietf.org/html/draft-hammer-oauth-07#section-3.6)
391 # Spaces must be encoded with "%20" instead of "+"
392 return encoded_str.replace('+', '%20').replace('%7E', '~')
393
394 def sign_request(self, signature_method, consumer, token):
395 """Set the signature parameter to the result of sign."""
396
397 if 'oauth_consumer_key' not in self:
398 self['oauth_consumer_key'] = consumer.key
399
400 if token and 'oauth_token' not in self:
401 self['oauth_token'] = token.key
402
403 self['oauth_signature_method'] = signature_method.name
404 self['oauth_signature'] = signature_method.sign(self, consumer, token)
405
406 @classmethod
407 def make_timestamp(cls):
408 """Get seconds since epoch (UTC)."""
409 return str(int(time.time()))
410
411 @classmethod
412 def make_nonce(cls):
413 """Generate pseudorandom number."""
414 return str(random.randint(0, 100000000))
415
416 @classmethod
417 def from_request(cls, http_method, http_url, headers=None, parameters=None,
418 query_string=None):
419 """Combines multiple parameter sources."""
420 if parameters is None:
421 parameters = {}
422
423 # Headers
424 if headers and 'Authorization' in headers:
425 auth_header = headers['Authorization']
426 # Check that the authorization header is OAuth.
427 if auth_header[:6] == 'OAuth ':
428 auth_header = auth_header[6:]
429 try:
430 # Get the parameters from the header.
431 header_params = cls._split_header(auth_header)
432 parameters.update(header_params)
433 except:
434 raise Error('Unable to parse OAuth parameters from '
435 'Authorization header.')
436
437 # GET or POST query string.
438 if query_string:
439 query_params = cls._split_url_string(query_string)
440 parameters.update(query_params)
441
442 # URL parameters.
443 param_str = urlparse.urlparse(http_url)[4] # query
444 url_params = cls._split_url_string(param_str)
445 parameters.update(url_params)
446
447 if parameters:
448 return cls(http_method, http_url, parameters)
449
450 return None
451
452 @classmethod
453 def from_consumer_and_token(cls, consumer, token=None,
454 http_method=HTTP_METHOD, http_url=None, parameters=None):
455 if not parameters:
456 parameters = {}
457
458 defaults = {
459 'oauth_consumer_key': consumer.key,
460 'oauth_timestamp': cls.make_timestamp(),
461 'oauth_nonce': cls.make_nonce(),
462 'oauth_version': cls.version,
463 }
464
465 defaults.update(parameters)
466 parameters = defaults
467
468 if token:
469 parameters['oauth_token'] = token.key
470 if token.verifier:
471 parameters['oauth_verifier'] = token.verifier
472
473 return Request(http_method, http_url, parameters)
474
475 @classmethod
476 def from_token_and_callback(cls, token, callback=None,
477 http_method=HTTP_METHOD, http_url=None, parameters=None):
478
479 if not parameters:
480 parameters = {}
481
482 parameters['oauth_token'] = token.key
483
484 if callback:
485 parameters['oauth_callback'] = callback
486
487 return cls(http_method, http_url, parameters)
488
489 @staticmethod
490 def _split_header(header):
491 """Turn Authorization: header into parameters."""
492 params = {}
493 parts = header.split(',')
494 for param in parts:
495 # Ignore realm parameter.
496 if param.find('realm') > -1:
497 continue
498 # Remove whitespace.
499 param = param.strip()
500 # Split key-value.
501 param_parts = param.split('=', 1)
502 # Remove quotes and unescape the value.
503 params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
504 return params
505
506 @staticmethod
507 def _split_url_string(param_str):
508 """Turn URL string into parameters."""
509 parameters = parse_qs(param_str, keep_blank_values=False)
510 for k, v in parameters.iteritems():
511 parameters[k] = urllib.unquote(v[0])
512 return parameters
513
514
515 class Client(httplib2.Http):
516 """OAuthClient is a worker to attempt to execute a request."""
517
518 def __init__(self, consumer, token=None, cache=None, timeout=None,
519 proxy_info=None):
520
521 if consumer is not None and not isinstance(consumer, Consumer):
522 raise ValueError("Invalid consumer.")
523
524 if token is not None and not isinstance(token, Token):
525 raise ValueError("Invalid token.")
526
527 self.consumer = consumer
528 self.token = token
529 self.method = SignatureMethod_HMAC_SHA1()
530
531 httplib2.Http.__init__(self, cache=cache, timeout=timeout,
532 proxy_info=proxy_info)
533
534 def set_signature_method(self, method):
535 if not isinstance(method, SignatureMethod):
536 raise ValueError("Invalid signature method.")
537
538 self.method = method
539
540 def request(self, uri, method="GET", body=None, headers=None,
541 redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None):
542 DEFAULT_CONTENT_TYPE = 'application/x-www-form-urlencoded'
543
544 if not isinstance(headers, dict):
545 headers = {}
546
547 is_multipart = method == 'POST' and headers.get('Content-Type',
548 DEFAULT_CONTENT_TYPE) != DEFAULT_CONTENT_TYPE
549
550 if body and method == "POST" and not is_multipart:
551 parameters = dict(parse_qsl(body))
552 else:
553 parameters = None
554
555 req = Request.from_consumer_and_token(self.consumer,
556 token=self.token, http_method=method, http_url=uri,
557 parameters=parameters)
558
559 req.sign_request(self.method, self.consumer, self.token)
560
561 if method == "POST":
562 headers['Content-Type'] = headers.get('Content-Type',
563 DEFAULT_CONTENT_TYPE)
564 if is_multipart:
565 headers.update(req.to_header())
566 else:
567 body = req.to_postdata()
568 elif method == "GET":
569 uri = req.to_url()
570 else:
571 headers.update(req.to_header())
572
573 return httplib2.Http.request(self, uri, method=method, body=body,
574 headers=headers, redirections=redirections,
575 connection_type=connection_type)
576
577
578 class Server(object):
579 """A skeletal implementation of a service provider, providing protected
580 resources to requests from authorized consumers.
581
582 This class implements the logic to check requests for authorization. You
583 can use it with your web server or web framework to protect certain
584 resources with OAuth.
585 """
586
587 timestamp_threshold = 300 # In seconds, five minutes.
588 version = VERSION
589 signature_methods = None
590
591 def __init__(self, signature_methods=None):
592 self.signature_methods = signature_methods or {}
593
594 def add_signature_method(self, signature_method):
595 self.signature_methods[signature_method.name] = signature_method
596 return self.signature_methods
597
598 def verify_request(self, request, consumer, token):
599 """Verifies an api call and checks all the parameters."""
600
601 version = self._get_version(request)
602 self._check_signature(request, consumer, token)
603 parameters = request.get_nonoauth_parameters()
604 return parameters
605
606 def build_authenticate_header(self, realm=''):
607 """Optional support for the authenticate header."""
608 return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
609
610 def _get_version(self, request):
611 """Verify the correct version request for this server."""
612 try:
613 version = request.get_parameter('oauth_version')
614 except:
615 version = VERSION
616
617 if version and version != self.version:
618 raise Error('OAuth version %s not supported.' % str(version))
619
620 return version
621
622 def _get_signature_method(self, request):
623 """Figure out the signature with some defaults."""
624 try:
625 signature_method = request.get_parameter('oauth_signature_method')
626 except:
627 signature_method = SIGNATURE_METHOD
628
629 try:
630 # Get the signature method object.
631 signature_method = self.signature_methods[signature_method]
632 except:
633 signature_method_names = ', '.join(self.signature_methods.keys())
634 raise Error('Signature method %s not supported try one of the follow ing: %s' % (signature_method, signature_method_names))
635
636 return signature_method
637
638 def _get_verifier(self, request):
639 return request.get_parameter('oauth_verifier')
640
641 def _check_signature(self, request, consumer, token):
642 timestamp, nonce = request._get_timestamp_nonce()
643 self._check_timestamp(timestamp)
644 signature_method = self._get_signature_method(request)
645
646 try:
647 signature = request.get_parameter('oauth_signature')
648 except:
649 raise MissingSignature('Missing oauth_signature.')
650
651 # Validate the signature.
652 valid = signature_method.check(request, consumer, token, signature)
653
654 if not valid:
655 key, base = signature_method.signing_base(request, consumer, token)
656
657 raise Error('Invalid signature. Expected signature base '
658 'string: %s' % base)
659
660 built = signature_method.sign(request, consumer, token)
661
662 def _check_timestamp(self, timestamp):
663 """Verify that timestamp is recentish."""
664 timestamp = int(timestamp)
665 now = int(time.time())
666 lapsed = now - timestamp
667 if lapsed > self.timestamp_threshold:
668 raise Error('Expired timestamp: given %d and now %s has a '
669 'greater difference than threshold %d' % (timestamp, now,
670 self.timestamp_threshold))
671
672
673 class SignatureMethod(object):
674 """A way of signing requests.
675
676 The OAuth protocol lets consumers and service providers pick a way to sign
677 requests. This interface shows the methods expected by the other `oauth`
678 modules for signing requests. Subclass it and implement its methods to
679 provide a new way to sign requests.
680 """
681
682 def signing_base(self, request, consumer, token):
683 """Calculates the string that needs to be signed.
684
685 This method returns a 2-tuple containing the starting key for the
686 signing and the message to be signed. The latter may be used in error
687 messages to help clients debug their software.
688
689 """
690 raise NotImplementedError
691
692 def sign(self, request, consumer, token):
693 """Returns the signature for the given request, based on the consumer
694 and token also provided.
695
696 You should use your implementation of `signing_base()` to build the
697 message to sign. Otherwise it may be less useful for debugging.
698
699 """
700 raise NotImplementedError
701
702 def check(self, request, consumer, token, signature):
703 """Returns whether the given signature is the correct signature for
704 the given consumer and token signing the given request."""
705 built = self.sign(request, consumer, token)
706 return built == signature
707
708
709 class SignatureMethod_HMAC_SHA1(SignatureMethod):
710 name = 'HMAC-SHA1'
711
712 def signing_base(self, request, consumer, token):
713 if request.normalized_url is None:
714 raise ValueError("Base URL for request is not set.")
715
716 sig = (
717 escape(request.method),
718 escape(request.normalized_url),
719 escape(request.get_normalized_parameters()),
720 )
721
722 key = '%s&' % escape(consumer.secret)
723 if token:
724 key += escape(token.secret)
725 raw = '&'.join(sig)
726 return key, raw
727
728 def sign(self, request, consumer, token):
729 """Builds the base signature string."""
730 key, raw = self.signing_base(request, consumer, token)
731
732 # HMAC object.
733 try:
734 from hashlib import sha1 as sha
735 except ImportError:
736 import sha # Deprecated
737
738 hashed = hmac.new(key, raw, sha)
739
740 # Calculate the digest base 64.
741 return binascii.b2a_base64(hashed.digest())[:-1]
742
743
744 class SignatureMethod_PLAINTEXT(SignatureMethod):
745
746 name = 'PLAINTEXT'
747
748 def signing_base(self, request, consumer, token):
749 """Concatenates the consumer key and secret with the token's
750 secret."""
751 sig = '%s&' % escape(consumer.secret)
752 if token:
753 sig = sig + escape(token.secret)
754 return sig, sig
755
756 def sign(self, request, consumer, token):
757 key, raw = self.signing_base(request, consumer, token)
758 return raw
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698