MythTV master
oauth_api.py
Go to the documentation of this file.
1"""
2The MIT License
3
4Copyright (c) 2007 Leah Culver
5
6Permission is hereby granted, free of charge, to any person obtaining a copy
7of this software and associated documentation files (the "Software"), to deal
8in the Software without restriction, including without limitation the rights
9to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10copies of the Software, and to permit persons to whom the Software is
11furnished to do so, subject to the following conditions:
12
13The above copyright notice and this permission notice shall be included in
14all copies or substantial portions of the Software.
15
16THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22THE SOFTWARE.
23"""
24
25import cgi
26import urllib.request, urllib.parse, urllib.error
27import time
28import random
29import hmac
30import binascii
31
32
33VERSION = '1.0' # Hi Blaine!
34HTTP_METHOD = 'GET'
35SIGNATURE_METHOD = 'PLAINTEXT'
36
37
38class OAuthError(RuntimeError):
39 """Generic exception class."""
40 def __init__(self, message='OAuth error occured.'):
41 self.message = message
42
44 """Optional WWW-Authenticate header (401 error)"""
45 return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
46
47def escape(s):
48 """Escape a URL including any /."""
49 return urllib.parse.quote(s, safe='~')
50
51def _utf8_str(s):
52 """Convert unicode to utf-8."""
53 if isinstance(s, str):
54 return s.encode("utf-8")
55 else:
56 return str(s)
57
59 """Get seconds since epoch (UTC)."""
60 return int(time.time())
61
62def generate_nonce(length=8):
63 """Generate pseudorandom number."""
64 return ''.join([str(random.randint(0, 9)) for i in range(length)])
65
66def generate_verifier(length=8):
67 """Generate pseudorandom number."""
68 return ''.join([str(random.randint(0, 9)) for i in range(length)])
69
70
71class OAuthConsumer(object):
72 """Consumer of OAuth authentication.
73
74 OAuthConsumer is a data type that represents the identity of the Consumer
75 via its shared secret with the Service Provider.
76
77 """
78 key = None
79 secret = None
80
81 def __init__(self, key, secret):
82 self.key = key
83 self.secret = secret
84
85
86class OAuthToken(object):
87 """OAuthToken is a data type that represents an End User via either an access
88 or request token.
89
90 key -- the token
91 secret -- the token secret
92
93 """
94 key = None
95 secret = None
96 callback = None
97 callback_confirmed = None
98 verifier = None
99
100 def __init__(self, key, secret):
101 self.key = key
102 self.secret = secret
103
104 def set_callback(self, callback):
105 self.callback = callback
106 self.callback_confirmed = 'true'
107
108 def set_verifier(self, verifier=None):
109 if verifier is not None:
110 self.verifier = verifier
111 else:
113
115 if self.callback and self.verifier:
116 # Append the oauth_verifier.
117 parts = urllib.parse.urlparse(self.callback)
118 scheme, netloc, path, params, query, fragment = parts[:6]
119 if query:
120 query = '%s&oauth_verifier=%s' % (query, self.verifier)
121 else:
122 query = 'oauth_verifier=%s' % self.verifier
123 return urllib.parse.urlunparse((scheme, netloc, path, params,
124 query, fragment))
125 return self.callback
126
127 def to_string(self):
128 data = {
129 'oauth_token': self.key,
130 'oauth_token_secret': self.secret,
131 }
132 if self.callback_confirmed is not None:
133 data['oauth_callback_confirmed'] = self.callback_confirmed
134 return urllib.parse.urlencode(data)
135
137 """ Returns a token from something like:
138 oauth_token_secret=xxx&oauth_token=xxx
139 """
140 params = cgi.parse_qs(s, keep_blank_values=False)
141 key = params['oauth_token'][0]
142 secret = params['oauth_token_secret'][0]
143 token = OAuthToken(key, secret)
144 try:
145 token.callback_confirmed = params['oauth_callback_confirmed'][0]
146 except KeyError:
147 pass # 1.0, no callback confirmed.
148 return token
149 from_string = staticmethod(from_string)
150
151 def __str__(self):
152 return self.to_string()
153
154
155class OAuthRequest(object):
156 """OAuthRequest represents the request and can be serialized.
157
158 OAuth parameters:
159 - oauth_consumer_key
160 - oauth_token
161 - oauth_signature_method
162 - oauth_signature
163 - oauth_timestamp
164 - oauth_nonce
165 - oauth_version
166 - oauth_verifier
167 ... any additional parameters, as defined by the Service Provider.
168 """
169 parameters = None # OAuth parameters.
170 http_method = HTTP_METHOD
171 http_url = None
172 version = VERSION
173
174 def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None):
175 self.http_method = http_method
176 self.http_url = http_url
177 self.parameters = parameters or {}
178
179 def set_parameter(self, parameter, value):
180 self.parameters[parameter] = value
181
182 def get_parameter(self, parameter):
183 try:
184 return self.parameters[parameter]
185 except:
186 raise OAuthError('Parameter not found: %s' % parameter)
187
189 return self.get_parameter('oauth_timestamp'), self.get_parameter(
190 'oauth_nonce')
191
193 """Get any non-OAuth parameters."""
194 parameters = {}
195 for k, v in list(self.parameters.items()):
196 # Ignore oauth parameters.
197 if k.find('oauth_') < 0:
198 parameters[k] = v
199 return parameters
200
201 def to_header(self, realm=''):
202 """Serialize as a header for an HTTPAuth request."""
203 auth_header = 'OAuth realm="%s"' % realm
204 # Add the oauth parameters.
205 if self.parameters:
206 for k, v in self.parameters.items():
207 if k[:6] == 'oauth_':
208 auth_header += ', %s="%s"' % (k, escape(str(v)))
209 return {'Authorization': auth_header}
210
211 def to_postdata(self):
212 """Serialize as post data for a POST request."""
213 return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) \
214 for k, v in self.parameters.items()])
215
216 def to_url(self):
217 """Serialize as a URL for a GET request."""
218 return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
219
221 """Return a string that contains the parameters that must be signed."""
222 params = self.parameters
223 try:
224 # Exclude the signature if it exists.
225 del params['oauth_signature']
226 except:
227 pass
228 # Escape key values before sorting.
229 key_values = [(escape(_utf8_str(k)), escape(_utf8_str(v))) \
230 for k,v in list(params.items())]
231 # Sort lexicographically, first after key, then after value.
232 key_values.sort()
233 # Combine key value pairs into a string.
234 return '&'.join(['%s=%s' % (k, v) for k, v in key_values])
235
237 """Uppercases the http method."""
238 return self.http_method.upper()
239
241 """Parses the URL and rebuilds it to be scheme://host/path."""
242 parts = urllib.parse.urlparse(self.http_url)
243 scheme, netloc, path = parts[:3]
244 # Exclude default port numbers.
245 if scheme == 'http' and netloc[-3:] == ':80':
246 netloc = netloc[:-3]
247 elif scheme == 'https' and netloc[-4:] == ':443':
248 netloc = netloc[:-4]
249 return '%s://%s%s' % (scheme, netloc, path)
250
251 def sign_request(self, signature_method, consumer, token):
252 """Set the signature parameter to the result of build_signature."""
253 # Set the signature method.
254 self.set_parameter('oauth_signature_method',
255 signature_method.get_name())
256 # Set the signature.
257 self.set_parameter('oauth_signature',
258 self.build_signature(signature_method, consumer, token))
259
260 def build_signature(self, signature_method, consumer, token):
261 """Calls the build signature method within the signature method."""
262 return signature_method.build_signature(self, consumer, token)
263
264 def from_request(http_method, http_url, headers=None, parameters=None,
265 query_string=None):
266 """Combines multiple parameter sources."""
267 if parameters is None:
268 parameters = {}
269
270 # Headers
271 if headers and 'Authorization' in headers:
272 auth_header = headers['Authorization']
273 # Check that the authorization header is OAuth.
274 if auth_header[:6] == 'OAuth ':
275 auth_header = auth_header[6:]
276 try:
277 # Get the parameters from the header.
278 header_params = OAuthRequest._split_header(auth_header)
279 parameters.update(header_params)
280 except:
281 raise OAuthError('Unable to parse OAuth parameters from '
282 'Authorization header.')
283
284 # GET or POST query string.
285 if query_string:
286 query_params = OAuthRequest._split_url_string(query_string)
287 parameters.update(query_params)
288
289 # URL parameters.
290 param_str = urllib.parse.urlparse(http_url)[4] # query
291 url_params = OAuthRequest._split_url_string(param_str)
292 parameters.update(url_params)
293
294 if parameters:
295 return OAuthRequest(http_method, http_url, parameters)
296
297 return None
298 from_request = staticmethod(from_request)
299
300 def from_consumer_and_token(oauth_consumer, token=None,
301 callback=None, verifier=None, http_method=HTTP_METHOD,
302 http_url=None, parameters=None):
303 if not parameters:
304 parameters = {}
305
306 defaults = {
307 'oauth_consumer_key': oauth_consumer.key,
308 'oauth_timestamp': generate_timestamp(),
309 'oauth_nonce': generate_nonce(),
310 'oauth_version': OAuthRequest.version,
311 }
312
313 defaults.update(parameters)
314 parameters = defaults
315
316 if token:
317 parameters['oauth_token'] = token.key
318 if token.callback:
319 parameters['oauth_callback'] = token.callback
320 # 1.0a support for verifier.
321 if verifier:
322 parameters['oauth_verifier'] = verifier
323 elif callback:
324 # 1.0a support for callback in the request token request.
325 parameters['oauth_callback'] = callback
326
327 return OAuthRequest(http_method, http_url, parameters)
328 from_consumer_and_token = staticmethod(from_consumer_and_token)
329
330 def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD,
331 http_url=None, parameters=None):
332 if not parameters:
333 parameters = {}
334
335 parameters['oauth_token'] = token.key
336
337 if callback:
338 parameters['oauth_callback'] = callback
339
340 return OAuthRequest(http_method, http_url, parameters)
341 from_token_and_callback = staticmethod(from_token_and_callback)
342
343 def _split_header(header):
344 """Turn Authorization: header into parameters."""
345 params = {}
346 parts = header.split(',')
347 for param in parts:
348 # Ignore realm parameter.
349 if param.find('realm') > -1:
350 continue
351 # Remove whitespace.
352 param = param.strip()
353 # Split key-value.
354 param_parts = param.split('=', 1)
355 # Remove quotes and unescape the value.
356 params[param_parts[0]] = urllib.parse.unquote(param_parts[1].strip('\"'))
357 return params
358 _split_header = staticmethod(_split_header)
359
360 def _split_url_string(param_str):
361 """Turn URL string into parameters."""
362 parameters = cgi.parse_qs(param_str, keep_blank_values=False)
363 for k, v in parameters.items():
364 parameters[k] = urllib.parse.unquote(v[0])
365 return parameters
366 _split_url_string = staticmethod(_split_url_string)
367
368class OAuthServer(object):
369 """A worker to check the validity of a request against a data store."""
370 timestamp_threshold = 300 # In seconds, five minutes.
371 version = VERSION
372 signature_methods = None
373 data_store = None
374
375 def __init__(self, data_store=None, signature_methods=None):
376 self.data_store = data_store
377 self.signature_methods = signature_methods or {}
378
379 def set_data_store(self, data_store):
380 self.data_store = data_store
381
382 def get_data_store(self):
383 return self.data_store
384
385 def add_signature_method(self, signature_method):
386 self.signature_methods[signature_method.get_name()] = signature_method
387 return self.signature_methods
388
389 def fetch_request_token(self, oauth_request):
390 """Processes a request_token request and returns the
391 request token on success.
392 """
393 try:
394 # Get the request token for authorization.
395 token = self._get_token(oauth_request, 'request')
396 except OAuthError:
397 # No token required for the initial token request.
398 version = self._get_version(oauth_request)
399 consumer = self._get_consumer(oauth_request)
400 try:
401 callback = self.get_callback(oauth_request)
402 except OAuthError:
403 callback = None # 1.0, no callback specified.
404 self._check_signature(oauth_request, consumer, None)
405 # Fetch a new token.
406 token = self.data_store.fetch_request_token(consumer, callback)
407 return token
408
409 def fetch_access_token(self, oauth_request):
410 """Processes an access_token request and returns the
411 access token on success.
412 """
413 version = self._get_version(oauth_request)
414 consumer = self._get_consumer(oauth_request)
415 try:
416 verifier = self._get_verifier(oauth_request)
417 except OAuthError:
418 verifier = None
419 # Get the request token.
420 token = self._get_token(oauth_request, 'request')
421 self._check_signature(oauth_request, consumer, token)
422 new_token = self.data_store.fetch_access_token(consumer, token, verifier)
423 return new_token
424
425 def verify_request(self, oauth_request):
426 """Verifies an api call and checks all the parameters."""
427 # -> consumer and token
428 version = self._get_version(oauth_request)
429 consumer = self._get_consumer(oauth_request)
430 # Get the access token.
431 token = self._get_token(oauth_request, 'access')
432 self._check_signature(oauth_request, consumer, token)
433 parameters = oauth_request.get_nonoauth_parameters()
434 return consumer, token, parameters
435
436 def authorize_token(self, token, user):
437 """Authorize a request token."""
438 return self.data_store.authorize_request_token(token, user)
439
440 def get_callback(self, oauth_request):
441 """Get the callback URL."""
442 return oauth_request.get_parameter('oauth_callback')
443
444 def build_authenticate_header(self, realm=''):
445 """Optional support for the authenticate header."""
446 return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
447
448 def _get_version(self, oauth_request):
449 """Verify the correct version request for this server."""
450 try:
451 version = oauth_request.get_parameter('oauth_version')
452 except:
453 version = VERSION
454 if version and version != self.version:
455 raise OAuthError('OAuth version %s not supported.' % str(version))
456 return version
457
458 def _get_signature_method(self, oauth_request):
459 """Figure out the signature with some defaults."""
460 try:
461 signature_method = oauth_request.get_parameter(
462 'oauth_signature_method')
463 except:
464 signature_method = SIGNATURE_METHOD
465 try:
466 # Get the signature method object.
467 signature_method = self.signature_methods[signature_method]
468 except:
469 signature_method_names = ', '.join(list(self.signature_methods.keys()))
470 raise OAuthError('Signature method %s not supported try one of the '
471 'following: %s' % (signature_method, signature_method_names))
472
473 return signature_method
474
475 def _get_consumer(self, oauth_request):
476 consumer_key = oauth_request.get_parameter('oauth_consumer_key')
477 consumer = self.data_store.lookup_consumer(consumer_key)
478 if not consumer:
479 raise OAuthError('Invalid consumer.')
480 return consumer
481
482 def _get_token(self, oauth_request, token_type='access'):
483 """Try to find the token for the provided request token key."""
484 token_field = oauth_request.get_parameter('oauth_token')
485 token = self.data_store.lookup_token(token_type, token_field)
486 if not token:
487 raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
488 return token
489
490 def _get_verifier(self, oauth_request):
491 return oauth_request.get_parameter('oauth_verifier')
492
493 def _check_signature(self, oauth_request, consumer, token):
494 timestamp, nonce = oauth_request._get_timestamp_nonce()
495 self._check_timestamp(timestamp)
496 self._check_nonce(consumer, token, nonce)
497 signature_method = self._get_signature_method(oauth_request)
498 try:
499 signature = oauth_request.get_parameter('oauth_signature')
500 except:
501 raise OAuthError('Missing signature.')
502 # Validate the signature.
503 valid_sig = signature_method.check_signature(oauth_request, consumer,
504 token, signature)
505 if not valid_sig:
506 key, base = signature_method.build_signature_base_string(
507 oauth_request, consumer, token)
508 raise OAuthError('Invalid signature. Expected signature base '
509 'string: %s' % base)
510 built = signature_method.build_signature(oauth_request, consumer, token)
511
512 def _check_timestamp(self, timestamp):
513 """Verify that timestamp is recentish."""
514 timestamp = int(timestamp)
515 now = int(time.time())
516 lapsed = abs(now - timestamp)
517 if lapsed > self.timestamp_threshold:
518 raise OAuthError('Expired timestamp: given %d and now %s has a '
519 'greater difference than threshold %d' %
520 (timestamp, now, self.timestamp_threshold))
521
522 def _check_nonce(self, consumer, token, nonce):
523 """Verify that the nonce is uniqueish."""
524 nonce = self.data_store.lookup_nonce(consumer, token, nonce)
525 if nonce:
526 raise OAuthError('Nonce already used: %s' % str(nonce))
527
528
529class OAuthClient(object):
530 """OAuthClient is a worker to attempt to execute a request."""
531 consumer = None
532 token = None
533
534 def __init__(self, oauth_consumer, oauth_token):
535 self.consumer = oauth_consumer
536 self.token = oauth_token
537
538 def get_consumer(self):
539 return self.consumer
540
541 def get_token(self):
542 return self.token
543
544 def fetch_request_token(self, oauth_request):
545 """-> OAuthToken."""
546 raise NotImplementedError
547
548 def fetch_access_token(self, oauth_request):
549 """-> OAuthToken."""
550 raise NotImplementedError
551
552 def access_resource(self, oauth_request):
553 """-> Some protected resource."""
554 raise NotImplementedError
555
556
557class OAuthDataStore(object):
558 """A database abstraction used to lookup consumers and tokens."""
559
560 def lookup_consumer(self, key):
561 """-> OAuthConsumer."""
562 raise NotImplementedError
563
564 def lookup_token(self, oauth_consumer, token_type, token_token):
565 """-> OAuthToken."""
566 raise NotImplementedError
567
568 def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
569 """-> OAuthToken."""
570 raise NotImplementedError
571
572 def fetch_request_token(self, oauth_consumer, oauth_callback):
573 """-> OAuthToken."""
574 raise NotImplementedError
575
576 def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
577 """-> OAuthToken."""
578 raise NotImplementedError
579
580 def authorize_request_token(self, oauth_token, user):
581 """-> OAuthToken."""
582 raise NotImplementedError
583
584
586 """A strategy class that implements a signature method."""
587 def get_name(self):
588 """-> str."""
589 raise NotImplementedError
590
591 def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
592 """-> str key, str raw."""
593 raise NotImplementedError
594
595 def build_signature(self, oauth_request, oauth_consumer, oauth_token):
596 """-> str."""
597 raise NotImplementedError
598
599 def check_signature(self, oauth_request, consumer, token, signature):
600 built = self.build_signature(oauth_request, consumer, token)
601 return built == signature
602
603
605
606 def get_name(self):
607 return 'HMAC-SHA1'
608
609 def build_signature_base_string(self, oauth_request, consumer, token):
610 sig = (
611 escape(oauth_request.get_normalized_http_method()),
612 escape(oauth_request.get_normalized_http_url()),
613 escape(oauth_request.get_normalized_parameters()),
614 )
615
616 key = '%s&' % escape(consumer.secret)
617 if token:
618 key += escape(token.secret)
619 raw = '&'.join(sig)
620 return key, raw
621
622 def build_signature(self, oauth_request, consumer, token):
623 """Builds the base signature string."""
624 key, raw = self.build_signature_base_stringbuild_signature_base_string(oauth_request, consumer,
625 token)
626
627 # HMAC object.
628 try:
629 import hashlib # 2.5
630 hashed = hmac.new(key, raw, hashlib.sha1)
631 except:
632 import sha # Deprecated
633 hashed = hmac.new(key, raw, sha)
634
635 # Calculate the digest base 64.
636 return binascii.b2a_base64(hashed.digest())[:-1]
637
638
640
641 def get_name(self):
642 return 'PLAINTEXT'
643
644 def build_signature_base_string(self, oauth_request, consumer, token):
645 """Concatenates the consumer key and secret."""
646 sig = '%s&' % escape(consumer.secret)
647 if token:
648 sig = sig + escape(token.secret)
649 return sig, sig
650
651 def build_signature(self, oauth_request, consumer, token):
652 key, raw = self.build_signature_base_stringbuild_signature_base_string(oauth_request, consumer,
653 token)
654 return key
def __init__(self, oauth_consumer, oauth_token)
Definition: oauth_api.py:534
def fetch_request_token(self, oauth_request)
Definition: oauth_api.py:544
def fetch_request_token(self, oauth_consumer, oauth_callback)
Definition: oauth_api.py:572
def lookup_token(self, oauth_consumer, token_type, token_token)
Definition: oauth_api.py:564
def lookup_nonce(self, oauth_consumer, oauth_token, nonce)
Definition: oauth_api.py:568
def authorize_request_token(self, oauth_token, user)
Definition: oauth_api.py:580
def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier)
Definition: oauth_api.py:576
def __init__(self, message='OAuth error occured.')
Definition: oauth_api.py:40
def set_parameter(self, parameter, value)
Definition: oauth_api.py:179
def sign_request(self, signature_method, consumer, token)
Definition: oauth_api.py:251
def build_signature(self, signature_method, consumer, token)
Definition: oauth_api.py:260
def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None)
Definition: oauth_api.py:174
def fetch_request_token(self, oauth_request)
Definition: oauth_api.py:389
def _get_token(self, oauth_request, token_type='access')
Definition: oauth_api.py:482
def add_signature_method(self, signature_method)
Definition: oauth_api.py:385
def _check_nonce(self, consumer, token, nonce)
Definition: oauth_api.py:522
def _get_signature_method(self, oauth_request)
Definition: oauth_api.py:458
def __init__(self, data_store=None, signature_methods=None)
Definition: oauth_api.py:375
def _check_signature(self, oauth_request, consumer, token)
Definition: oauth_api.py:493
def build_signature(self, oauth_request, consumer, token)
Definition: oauth_api.py:622
def build_signature_base_string(self, oauth_request, consumer, token)
Definition: oauth_api.py:609
def build_signature_base_string(self, oauth_request, consumer, token)
Definition: oauth_api.py:644
def build_signature(self, oauth_request, consumer, token)
Definition: oauth_api.py:651
def build_signature(self, oauth_request, oauth_consumer, oauth_token)
Definition: oauth_api.py:595
def check_signature(self, oauth_request, consumer, token, signature)
Definition: oauth_api.py:599
def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token)
Definition: oauth_api.py:591